//-----------------------------------------------------------------------
// <copyright file="CreateSourceFiles.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation.  All rights reserved.
// THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY 
// KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
// PARTICULAR PURPOSE.
// </copyright>
// <author>Andy Reeves</author>
// <email>andyr</email>
// <date>2005-03-22</date>
// <summary>Creates the versionnumber.cs and versionnumber.h files.</summary>
//-----------------------------------------------------------------------

namespace Microsoft.Sdc.Tasks.VersionNumber
{
    using System;
    using System.IO;
    using System.Xml;
    using System.Globalization;
    using System.Text;
    using System.CodeDom;
    using System.CodeDom.Compiler;
    using System.Reflection;

    using Microsoft.Build.Framework;

    /// <summary>
    /// Creates the VersionNumber.cs and VersionNumber.h files from the config file provided.
    /// Typically these files are then linked in to each of your projects
    /// If you provide BuildNumber and RevisionNumber then your provided values are used and not the values
    /// from the config file
    /// </summary>
    /// <remarks>
    /// <code><![CDATA[
    /// <VersionNumber.CreateSourceFiles
    ///             OutputPath="outputPath"
    ///             ConfigFileLocation="configFileLocation"
    ///             Filename="filename"
    ///             BuildNumber="buildNumber"
    ///             RevisionNumber="revisionNumber"
    /// </VersionNumber.CreateSourceFiles>
    /// ]]></code>
    /// <para>where:</para>
    /// <para><i>outputPath</i></para>
    /// <para>The path to the folder where VersionNumber.cs and VersionNumber.h will be written.</para>
    /// <para><i>filename</i></para>
    /// <para>The filename to use as the root for the generated files. Defaults to VersionNumber</para>
    /// <para><i>configFileLocation</i></para>
    /// <para>Fully qualified file name to the xml config file. The file contains, in Xml format, the major, minor, build and revision number.</para>
    /// <para><i>buildNumber</i></para>
    /// <para>If you set this value then the value from the config file is ignored.</para>
    /// <para><i>revisionNumber</i></para>
    /// <para>If you set this value then the value from the config file is ignored.</para>
    ///
    /// For example:
    /// 
    /// <example>
    /// <code><![CDATA[
    /// <?xml version="1.0" encoding="utf-8"?>
    ///     <configuration>
    ///         <appSettings>
    ///             <add key="language" value="C++;C#" />    
    ///             <add key="major" value="2" />
    ///             <add key="minor" value="0" />
    ///             <add key="build" value="050304" />
    ///             <add key="revision" value="001" />
    ///         </appSettings>
    ///     </configuration>
    /// ]]></code>    
    /// </example>
    /// </remarks>
    /// <example>
    /// <code><![CDATA[
    /// <Project>
    ///     <Target Name="Test" >
    ///         <VersionNumber.CreateSourceFiles
    ///               ConfigFileLocation="@(VersionXmlFile)" 
    ///               BuildNumber="051203"
    ///               RevisionNumber="00002"
    ///               OutputPath="$(MSBuildBinPath)\..\Common" >
    ///         </VersionNumber.CreateSourceFiles> 
    ///     </Target>
    /// </Project>
    /// ]]></code>    
    /// </example>
    public class CreateSourceFiles : TaskBase
    {
        private string versionNumberConfigFileLocation;
        private string revisionBuildNumber;
        private int majorBuildNumber;
        private int minorBuildNumber;
        private string buildBuildNumber;
        private DateTime startDate;
        private string buildNumberFull;
        private string languages;
        private string filename = "VersionNumber";
        private string outputPath;

        private readonly int languageNumDefault = 0;
        
        // Initialise the list of language options supported and their equivalent code providers.
        private string[] supportedLanguages =
        {
            "c#",
            "csharp",
            "vb",
            "visualbasic",
            "c++",
            "cplusplus"
        };

        private string[] extension = 
               {
                   ".cs",
                   ".cs",
                   ".vb",
                   ".vb",
                   ".h",
                   ".h"
               };

        private CodeDomProvider[] codeDomProviders =
        {
            new Microsoft.CSharp.CSharpCodeProvider(), 
            new Microsoft.CSharp.CSharpCodeProvider(),
            new Microsoft.VisualBasic.VBCodeProvider(),
            new Microsoft.VisualBasic.VBCodeProvider(),
            new CppCodeDomProvider(),
            new CppCodeDomProvider()
        };
        
        private XmlDocument versionConfig = new XmlDocument();

        /// <summary>
        /// Initializes a new instance of the CreateSourceFiles class.
        /// </summary>
        public CreateSourceFiles()
        {
        }

        /// <summary>
        /// Performs the action of this task.
        /// </summary>
        protected override void InternalExecute()
        {
            this.LoadFile();

            if (this.languages.Length == 0)
            {
                this.languages = this.supportedLanguages[this.languageNumDefault];
            }

            // Split the language list and process each one
            string[] languagesArray = languages.Split(';');
            foreach (string language in languagesArray)
            {
                int languageNum = 0;
                try
                {
                    languageNum = this.LookupLanguage(language);
                }
                catch (ArgumentException)
                {
                    Log.LogWarning("Invalid language \"{0}\" specified.", language);
                    Log.LogWarning("Language defaulted to \"{0}\"", this.supportedLanguages[this.languageNumDefault]);
                    languageNum = this.languageNumDefault;
                }

                string tempFile = this.Filename + this.extension[languageNum];
                tempFile = Path.Combine(this.OutputPath, tempFile);
                Log.LogMessage(MessageImportance.Low, "Generating file: " + tempFile);

                if (File.Exists(tempFile))
                {
                    //file exists so check to see if it contains the same details
                    StreamReader reader = File.OpenText(tempFile);
                    string s = reader.ReadToEnd();
                    reader.Close();

                    string textToWrite = this.GetSourceCode(languageNum);
                    if (s != (textToWrite))
                    {
                        this.CreateFile(languageNum, tempFile);
                    }
                }
                else
                {
                    //file doesn't exist so create it
                    this.CreateFile(languageNum, tempFile);
                }
            }
        }

        private void CreateFile(int languageNum, string file)
        {
            StreamWriter writer = File.CreateText(file);
            writer.Write(GetSourceCode(languageNum));
            writer.Close();
        }

        // Create a compile unit,
        // build the CodeDOM object graph of the required assembly file version attribute
        // create a string builder in which to insert the code, and create a stringwriter for that
        // Generate the code into the stringwriter, using the compile unit and language specific codeDomProvider
        // return the code now in the stringbuilder
        private string GetSourceCode(int languageNum)
        {
            CodeCompileUnit compileUnit = new CodeCompileUnit();
            this.BuildAssemblyFileVersionObjectGraph(compileUnit);
            StringBuilder stringBuilder = new StringBuilder();
            StringWriter stringWriter = new StringWriter(stringBuilder);
            this.GenerateCodeFromCompileUnit(stringWriter, codeDomProviders[languageNum], compileUnit);

            return stringBuilder.ToString();
        }

        private void BuildAssemblyFileVersionObjectGraph(CodeCompileUnit compileUnit)
        {
            StringBuilder stringBuilder = new StringBuilder();
            stringBuilder.Append(this.majorBuildNumber);
            stringBuilder.Append(".");
            stringBuilder.Append(this.minorBuildNumber);
            stringBuilder.Append(".");
            stringBuilder.Append(this.buildBuildNumber);
            stringBuilder.Append(".");
            stringBuilder.Append(this.revisionBuildNumber);

            compileUnit.ReferencedAssemblies.Add("System.Reflection");
            CodeAttributeArgument arg = new CodeAttributeArgument(new CodePrimitiveExpression(stringBuilder.ToString()));
            CodeAttributeDeclaration attr = new CodeAttributeDeclaration("System.Reflection.AssemblyFileVersion", arg);
            compileUnit.AssemblyCustomAttributes.Add(attr);
        }

        private void GenerateCodeFromCompileUnit(TextWriter textWriter, CodeDomProvider provider, CodeCompileUnit compileunit)
        {
            IndentedTextWriter indentedTextWriter = new IndentedTextWriter(textWriter, "    ");
            provider.GenerateCodeFromCompileUnit(compileunit, indentedTextWriter, new CodeGeneratorOptions());
            textWriter.Close();
        }

        // Simple lookup function...
        // Didn't seem worth the hassle of using a hash table for the language strings
        // An array is easier to construct, can be done alongside the codeDomProviders making things less error prone.
        // Also didn't seem worth creating a complex array to guarantee that the code providers and language strings match.
        private int LookupLanguage(string language)
        {
            string tempLang = language.ToLower();
            for (int languageNum = 0; languageNum < this.supportedLanguages.Length; languageNum++)
            {
                if (tempLang == this.supportedLanguages[languageNum])
                {
                    return languageNum;
                }
            }
            throw new ArgumentException("Invalid Language Specified: " + language);
        }

        private void LoadFile()
        {
            this.versionConfig.Load(this.versionNumberConfigFileLocation);

            XmlNode languageNode = this.versionConfig.SelectSingleNode("configuration/appSettings/add[@key='language']");
            if (languageNode != null)
            {
                this.languages = languageNode.Attributes["value"].Value;
            }

            XmlNode majorNode = this.versionConfig.SelectSingleNode("configuration/appSettings/add[@key='major']");
            if (majorNode != null)
            {
                this.majorBuildNumber = XmlConvert.ToInt32(majorNode.Attributes["value"].Value);
            }

            XmlNode minorNode = this.versionConfig.SelectSingleNode("configuration/appSettings/add[@key='minor']");
            if (minorNode != null)
            {
                this.minorBuildNumber = XmlConvert.ToInt32(minorNode.Attributes["value"].Value);
            }

            XmlNode buildNode = this.versionConfig.SelectSingleNode("configuration/appSettings/add[@key='build']");
            if (buildNode != null)
            {
                if (this.BuildNumber.Length == 0)
                {
                    this.buildBuildNumber = buildNode.Attributes["value"].Value;
                }
            }

            XmlNode revisionNode = this.versionConfig.SelectSingleNode("configuration/appSettings/add[@key='revision']");
            if (revisionNode != null)
            {
                if (this.RevisionNumber.Length == 0)
                {
                    this.revisionBuildNumber = revisionNode.Attributes["value"].Value;
                }
            }

            XmlNode startDateNode = this.versionConfig.SelectSingleNode("configuration/appSettings/add[@key='startDate']");
            if (startDateNode != null)
            {
                this.startDate = XmlConvert.ToDateTime(startDateNode.Attributes["value"].Value, "yyyy-MM-dd");
            }

            buildNumberFull = String.Format(CultureInfo.InvariantCulture, "{0}.{1}.{2}.{3}", this.majorBuildNumber, this.minorBuildNumber, this.buildBuildNumber, this.revisionBuildNumber);
        }

        /// <summary>
        /// The full path to the xml config file containing the version numbers
        /// </summary>
        [Required]
        public string ConfigFileLocation
        {
            get { return (this.versionNumberConfigFileLocation == null ? String.Empty : this.versionNumberConfigFileLocation); }
            set { this.versionNumberConfigFileLocation = value; }
        }

        /// <summary>
        /// The path to write the VersionNumber.cs file to
        /// </summary>
        [Required]
        public string OutputPath
        {
            get { return (this.outputPath == null ? String.Empty : this.outputPath); }
            set { this.outputPath = value; }
        }

        /// <summary>
        /// Defaults to VersionNumber if not specified
        /// </summary>
        public string Filename
        {
            get { return (this.filename == null ? String.Empty : this.filename); }
            set { this.filename = value; }
        }

        /// <summary>
        /// If this value is provided then the value from the config file is ignored
        /// </summary>
        public string RevisionNumber
        {
            get { return (this.revisionBuildNumber == null ? String.Empty : this.revisionBuildNumber); }
            set { this.revisionBuildNumber = value; }
        }

        /// <summary>
        /// If this value is provided then the value from the config file is ignored
        /// </summary>       
        public string BuildNumber
        {
            get { return (this.buildBuildNumber == null ? String.Empty : this.buildBuildNumber); }
            set { this.buildBuildNumber = value; }
        }

        /// <summary>
        /// Returns the full build number i.e. 2.0.050518.00002
        /// </summary>
        [Output]
        public string VersionNumber
        {
            get { return (this.buildNumberFull == null ? String.Empty : this.buildNumberFull); }
        }
    }
}

